package com.yjb.demo.elasticsearch.controller;

import cn.hutool.json.JSONUtil;
import com.yjb.demo.elasticsearch.common.ResponseResult;
import com.yjb.demo.elasticsearch.common.page.PageParam;
import com.yjb.demo.elasticsearch.common.page.PageResult;
import com.yjb.demo.elasticsearch.domain.doc.HotelDoc;
import com.yjb.demo.elasticsearch.domain.query.HotelQuery;
import io.swagger.annotations.ApiOperation;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.index.query.BoolQueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.SearchHits;
import org.elasticsearch.search.aggregations.AggregationBuilders;
import org.elasticsearch.search.aggregations.Aggregations;
import org.elasticsearch.search.aggregations.bucket.terms.Terms;
import org.elasticsearch.search.sort.SortBuilders;
import org.elasticsearch.search.sort.SortOrder;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;
import javax.validation.constraints.NotBlank;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import static com.yjb.demo.elasticsearch.common.ElasticsearchConstants.HOTEL_INDEX;

/**
 * @description 
 * @author yinjinbiao
 * @create 2024/1/12 9:40
 * @version 1.0
 */
@ApiOperation("全文检索Demo")
@RestController
@RequestMapping("/search")
public class SearchController {

	@Resource
	private RestHighLevelClient client;

	/**
	 * es不给查询所有值，默认只给前10条。
	 * @return
	 * @throws IOException
	 */
	@ApiOperation("查询所有，es不给查询所有值，默认只给前10条。")
	@GetMapping("/searchAll")
	public ResponseResult searchAll() throws IOException {

		// 1.准备Request
		SearchRequest request = new SearchRequest(HOTEL_INDEX);
		// 2.准备DSL
		request.source()
				.query(QueryBuilders.matchAllQuery());
		// 3.发送请求
		SearchResponse response = response = client.search(request, RequestOptions.DEFAULT);

		PageResult pageResult = parseSearchResponse(response);
		return ResponseResult.ok(pageResult);
	}

	/**
	 * 模糊查询all中带keyword的数据
	 * all是一个组合字段，其目的是将多字段的值 利用copy_to合并，提供给用户搜索
	 * @param keyword
	 * @return
	 * @throws IOException
	 */
	@ApiOperation("模糊查询组合字段all中带keyword的数据")
	@GetMapping("/searchByKeyword")
	public ResponseResult searchByKeyword(@NotBlank String keyword) throws IOException {

		// 1.准备Request
		SearchRequest request = new SearchRequest(HOTEL_INDEX);
		// 2.准备DSL
		request.source()
				.query(QueryBuilders.matchQuery("all", keyword));
		// 3.发送请求
		SearchResponse response = client.search(request, RequestOptions.DEFAULT);

		PageResult pageResult = parseSearchResponse(response);
		return ResponseResult.ok(pageResult);
	}

	/**
	 * 实现4个属性的聚合搜索
	 * @param hotelQuery
	 * @return
	 * @throws IOException
	 */
	@ApiOperation("模拟一个酒店全文检索的业务，按3个维度检索出不同的品牌、星级、城市")
	@GetMapping("/searchAggs")
	public ResponseResult searchAggs(HotelQuery hotelQuery) throws IOException {
		try {
			// 1.准备Request
			SearchRequest request = new SearchRequest(HOTEL_INDEX);
			// 2.准备DSL
			// 2.1.query
			//  构建BooleanQuery
			BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
			//  关键字搜索
			String key = hotelQuery.getKey();
			if (key == null || "".equals(key)) {
				boolQuery.must(QueryBuilders.matchAllQuery());
			} else {
				boolQuery.must(QueryBuilders.matchQuery("all", key));
			}
			// 2.2.设置size
			request.source().size(0);
			// 2.3.聚合
			request.source().query(boolQuery).aggregation(AggregationBuilders
					.terms("brandAgg")
					.field("brand")
					.size(100)
			);
			request.source().aggregation(AggregationBuilders
					.terms("cityAgg")
					.field("city")
					.size(100)
			);
			request.source().aggregation(AggregationBuilders
					.terms("starAgg")
					.field("starName")
					.size(100)
			);
			// 3.发出请求
			SearchResponse response = client.search(request, RequestOptions.DEFAULT);
			// 4.解析结果
			Map<String, List<String>> result = new HashMap<>();
			Aggregations aggregations = response.getAggregations();
			// 4.1.根据品牌名称，获取品牌结果
			List<String> brandList = getAggByName(aggregations, "brandAgg");
			result.put("品牌", brandList);
			// 4.2.根据品牌名称，获取品牌结果
			List<String> cityList = getAggByName(aggregations, "cityAgg");
			result.put("城市", cityList);
			// 4.3.根据品牌名称，获取品牌结果
			List<String> starList = getAggByName(aggregations, "starAgg");
			result.put("星级", starList);

			return ResponseResult.ok(result);
		} catch (IOException e) {
			throw new RuntimeException(e);
		}

	}

	/**
	 * 复合分页查询
	 * 条件：城市、星级、品牌、价格
	 * 排序：价格
	 * @param pageParam
	 * @param hotelQuery
	 * @return
	 * @throws IOException
	 */
	@ApiOperation("模拟一个酒店全文检索的业务，按照keyword、所选条件：城市、星级、品牌、价格，排序：价格，分页查询")
	@GetMapping("/searchBooleanQuery")
	public ResponseResult searchBooleanQuery(PageParam pageParam, HotelQuery hotelQuery) throws IOException {
		//  准备Request
		SearchRequest request = new SearchRequest(HOTEL_INDEX);
		//  构建BooleanQuery
		BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
		//  关键字搜索
		String key = hotelQuery.getKey();
		if (key == null || "".equals(key)) {
			boolQuery.must(QueryBuilders.matchAllQuery());
		} else {
			boolQuery.must(QueryBuilders.matchQuery("all", key));
		}
		//  城市条件
		if (hotelQuery.getCity() != null && !hotelQuery.getCity().equals("")) {
			boolQuery.filter(QueryBuilders.termQuery("city", hotelQuery.getCity()));
		}
		//  品牌条件
		if (hotelQuery.getBrand() != null && !hotelQuery.getBrand().equals("")) {
			boolQuery.filter(QueryBuilders.termQuery("brand", hotelQuery.getBrand()));
		}
		//  星级条件
		if (hotelQuery.getStarName() != null && !hotelQuery.getStarName().equals("")) {
			boolQuery.filter(QueryBuilders.termQuery("starName", hotelQuery.getStarName()));
		}
		//  价格
		if (hotelQuery.getMinPrice() != null && hotelQuery.getMaxPrice() != null) {
			boolQuery.filter(QueryBuilders
					.rangeQuery("price")
					.gte(hotelQuery.getMinPrice())
					.lte(hotelQuery.getMaxPrice())
			);
		}
		//  加入分页，放入source，并按价格升序
		request.source()
				.from((pageParam.getCurrent()-1)*pageParam.getSize())
				.size(pageParam.getSize())
				.query(boolQuery)
				.sort(SortBuilders.fieldSort("price").order(SortOrder.ASC));
		// 发送请求
		SearchResponse response = client.search(request, RequestOptions.DEFAULT);
		PageResult pageResult = parseSearchResponse(response);
		return ResponseResult.ok(pageResult);
	}

	private List<String> getAggByName(Aggregations aggregations, String aggName) {
		// 4.1.根据聚合名称获取聚合结果
		Terms brandTerms = aggregations.get(aggName);
		// 4.2.获取buckets
		List<? extends Terms.Bucket> buckets = brandTerms.getBuckets();
		// 4.3.遍历
		List<String> brandList = new ArrayList<>();
		for (Terms.Bucket bucket : buckets) {
			// 4.4.获取key
			String key = bucket.getKeyAsString();
			brandList.add(key);
		}
		return brandList;
	}


	private PageResult parseSearchResponse(SearchResponse response){
		List<HotelDoc> hotelDocs = new ArrayList<>();
		// 4.解析响应
		SearchHits searchHits = response.getHits();
		// 4.1.获取总条数
		long total = searchHits.getTotalHits().value;
		// 4.2.文档数组
		SearchHit[] hits = searchHits.getHits();
		// 4.3.遍历
		for (SearchHit hit : hits) {
			// 获取文档source
			String json = hit.getSourceAsString();
			// 反序列化
			HotelDoc hotelDoc = JSONUtil.toBean(json, HotelDoc.class);
			hotelDocs.add(hotelDoc);
		}
		return new PageResult(hotelDocs, total);
	}

}
